﻿// Copyright 2010 Maxime Serrano <lastchronicler@gmail.com>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.Diagnostics;

using MCSharp.Network;

namespace MCSharp {
    public class Server {
        static List<Session> sessions = new List<Session>();
        static Dictionary<int, Player> players = new Dictionary<int, Player>(255);
        internal static Player[] playerList;
        static object playerListLock = new object();

        public static World world;
        static TcpListener listener;
        public static int maxUploadSpeed,   // set by Config.ApplyConfig
                          packetsPerSecond, // set by Config.ApplyConfig
                          maxSessionPacketsPerTick = 128,
                          maxBlockUpdatesPerTick = 60000; // used when there are no players in a world
        //internal static float ticksPerSecond;
        const int maxPortAttempts = 20;
        public static int port;
        public static string ServerID;
        public static readonly string WhiteListLocation = "whitelist.txt";
        public static List<string> whitelist = new List<string>();

        #region Init / Start / Stop

        public static void ResetWorkingDirectory() {
            // reset working directory to same folder as the executable
            Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
        }

        public static bool Init() {
            // Do this before it all
            PacketHandlers.Init();

            // Change working directory to be relative to the executable
            ResetWorkingDirectory();

            // try to load the config
            if (!Config.Load()) return false;
            Config.ApplyConfig();
            if (!Config.Save()) return false;

            // Generate a Server ID
            GenerateServerID();

            // load player DB
            PlayerDB.Load();

            // Load the whitelist (if whitelist mode enabled)
            LoadWhitelist();

            // Prepare the list of commands
            CommandList.Init();

            // start the task thread
            Tasks.Start();

            // Load the world
            world = new World();
            if (!world.LoadWorld(Config.GetString(ConfigKey.World))) {
                world = null;
                Logger.Log("Failed to load world!", LogType.FatalError);
                return false;
            }

            return true;
        }

        public static bool Start() {
            // Create Console player
            Player.Console = new Player("(console)");

            // open the port
            bool portFound = false;
            int attempts = 0;
            port = Config.GetInt( ConfigKey.Port );

            do {
                try {
                    listener = new TcpListener( IPAddress.Any, port );
                    listener.Start();
                    portFound = true;

                } catch( Exception ex ) {
                    // if the port is unavailable, try next one
                    Logger.Log( "Could not start listening on port {0}, trying next port. ({1})", LogType.Error,
                                   port, ex.Message );
                    port++;
                    attempts++;
                }
            } while( !portFound && attempts < maxPortAttempts );

            // if the port still cannot be opened after [maxPortAttempts] attemps, die.
            if( !portFound ) {
                Logger.Log( "Could not start listening after {0} tries. Giving up!", LogType.FatalError,
                               maxPortAttempts );
                return false;
            }

            serverStart = DateTime.Now;
            Logger.Log( "Server.Run: now accepting connections at port {0}.", LogType.Debug,
                        port);

            // Check for incoming connections 4 times per second
            AddTask(CheckConnections, 250);

            // Send keep-alive packets every 10 seconds
            AddTask(KeepAlive, 10000);

            // Send time update every 5 seconds to keep server in sync
            AddTask(SyncTime, 5000);

            // Update world time every second
            AddTask(UpdateTime, 1000);

            // Save world (environment and chunks) every 30 seconds
            AddTask(SaveWorld, 30000);

            // Write out initial (empty) playerlist cache
            UpdatePlayerList();

            // start the main loop - server is now connectible
            mainThread = new Thread(MainLoop);
            mainThread.Start();

            return true;
        }

        public static void Shutdown() {
            Logger.Log("Server shutting down.", LogType.SystemActivity);

            // Kick all players
            foreach (Player ply in playerList)
                ply.session.KickNow("Server shutting down");

            // kill the main thread
            shuttingDown = true;
            if (mainThread != null && mainThread.IsAlive) {
                mainThread.Join();
            }

            // stop accepting new players
            if (listener != null) {
                listener.Stop();
                listener = null;
            }

            // kill background tasks
            Tasks.Shutdown();

            // Save player DB
            PlayerDB.Save();

            // Save the world
            if (world != null)
                world.SaveWorld(Config.GetString(ConfigKey.World));
        }

        #endregion

        #region Scheduler

        static int taskIdCounter;
        static Dictionary<int, ScheduledTask> tasks = new Dictionary<int, ScheduledTask>();
        static ScheduledTask[] taskList;
        static Thread mainThread;
        static DateTime serverStart;
        public static bool shuttingDown;
        static object taskListLock = new object();

        internal static void MainLoop() {
            ScheduledTask[] taskCache;
            ScheduledTask task;
            while (!shuttingDown) {
                taskCache = taskList;
                for (int i = 0; i < taskCache.Length; i++) {
                    task = taskCache[i];
                    if (task.enabled && task.nextTime < DateTime.UtcNow) {
                        try {
                            task.callback(task.param);
                        } catch (Exception ex) {
                            Logger.Log("Server.MainLoop: Exception was thrown by a scheduled task. Normally this would crash the server. Exception details follow: " + ex, LogType.Error);
                        }
                        task.nextTime += TimeSpan.FromMilliseconds(task.interval);
                    }
                }
                Thread.Sleep(1);
            }
        }

        static void SaveMap(object param) {
        }

        internal static int AddTask(TaskCallback task, int interval) {
            return AddTask(task, interval, null, 0);
        }

        internal static int AddTask(TaskCallback task, int interval, object param) {
            return AddTask(task, interval, param, 0);
        }

        internal static int AddTask(TaskCallback task, int interval, object param, int delay) {
            ScheduledTask newTask = new ScheduledTask();
            newTask.nextTime = DateTime.UtcNow.AddMilliseconds(delay);
            newTask.callback = task;
            newTask.interval = interval;
            newTask.param = param;
            tasks.Add(++taskIdCounter, newTask);
            UpdateTaskListCache();
            return taskIdCounter;
        }

        internal static void TaskToggle(int id, bool enabled) {
            tasks[id].nextTime = DateTime.UtcNow;
            tasks[id].enabled = enabled;
            UpdateTaskListCache();
        }

        static void UpdateTaskListCache() {
            List<ScheduledTask> tempTaskList = new List<ScheduledTask>();
            lock (taskListLock) {
                foreach (ScheduledTask task in tasks.Values) {
                    if (task.enabled) {
                        tempTaskList.Add(task);
                    }
                }
            }
            taskList = tempTaskList.ToArray();
        }
        #endregion
        
        #region Networking

        public static void SendToAllDelayed( Packet packet, Player except ) {
        }

        public static void SendToAll( Packet packet ) {
            SendToAll( packet, null );
        }

        public static void SendToAll( Packet packet, Player except ) {
            Player[] tempList = playerList;
            for( int i = 0; i < tempList.Length; i++ ) {
                if( tempList[i] != except ) {
                    tempList[i].Send( packet );
                }
            }
        }

        public static void SendToAll( string message ) {
            SendToAll( ">", message, null );
        }

        public static void SendToAll( string prefix, string message ) {
            SendToAll( prefix, message, null );
        }

        public static void SendToAll( string message, Player except ) {
            SendToAll( ">", message, except );
        }

        public static void SendToAll( string prefix, string message, Player except ) {
            SendToAll(PacketWriter.MakeMessage(message), except);
        }
        
        // Broadcast to a specific class
        public static void SendToClass( Packet packet, PlayerClass playerClass ) {
        }

        // Send to all others in chunk range
        public static void SendToOthersInRange(Packet packet, Player player) {
            foreach (Player ply in playerList) {
                if (ply == player)
                    continue;
                if (!CommonChunks(ply, player))
                    continue;
                ply.Send(packet);
            }
        }

        // Send to all in chunk range (including self)
        public static void SendToAllInRange(Packet packet, Player player) {
            foreach (Player ply in playerList) {
                if (!CommonChunks(ply, player))
                    continue;
                ply.Send(packet);
            }
        }

        // Send keep-alive packet to keep client connected
        internal static void KeepAlive(object param) {
            SendToAll(PacketWriter.MakeKeepAlive());
        }

        // Send time update packet to keep time in sync across clients
        internal static void SyncTime(object param) {
            SendToAll(PacketWriter.MakeTimeUpdate(world.time));
        }

        // Update world time
        internal static void UpdateTime(object param) {
            if(Config.GetBool(ConfigKey.AllowTime))
                world.time += 20;
            while (world.time > 24000)
                world.time -= 24000;
        }
        
        // Save world (environment and chunks)
        internal static void SaveWorld(object param) {
            if (world != null)
                world.SaveWorld();
        }

        // checks for incoming connections and disposes old sessions
        internal static void CheckConnections( object param ) {
            if( listener.Pending() ) {
                try {
                    sessions.Add( new Session( listener.AcceptTcpClient() ) );
                } catch( Exception ex ) {
                    Logger.Log( "Server.CheckConnections: Could not accept incoming connection: " + ex, LogType.Error );
                }
            }
            // this loop does not need to be thread-safe since only mainthread can alter session list
            for( int i = 0; i < sessions.Count; i++ ) {
                if( sessions[i].canDispose ) {
                    sessions[i].Disconnect();
                    sessions.RemoveAt( i );
                    i--;
                    Logger.Log( "Server.CheckConnections: Session disposed. Active sessions left: {0}.", LogType.Debug, sessions.Count );
                    GC.Collect( GC.MaxGeneration, GCCollectionMode.Optimized );
                }
            }
        }
        #endregion

        #region Events

        public static event LogEventHandler OnLog;

        internal static void FireLogEvent(string message, LogType type) {
            if (OnLog != null) OnLog(message, type);
        }

        #endregion

        #region PlayerList

        public static void LoadWhitelist() {
            if (File.Exists("whitelist.txt")) {
                using (StreamReader whitelistFile = File.OpenText("whitelist.txt")) {
                    for (string line; (line = whitelistFile.ReadLine()) != null; ) {
                        int commentIndex = line.IndexOf('#');

                        if (commentIndex > 0)
                            line = line.Substring(0, commentIndex).Trim();
                        else if (commentIndex < 0)
                            line = line.Trim();
                        else
                            continue;

                        if (line != null && line.Length > 0)
                            whitelist.Add(line.ToLower());
                    }
                }
            } else {
                File.WriteAllLines(WhiteListLocation, new string[] {
                    "# If whitelist mode is enabled in the config, then only",
                    "# names specified in this file will be allowed to join.",
                    "# Write one name per line. Anything after a # is a comment.",
                    "# Blank lines are ignored. Not case-sensitive.",
                    "# Leading/Trailing tabs and spaces are ignored."
                });
            }
        }

        public static void AddToWhitelist(string name) {
            if (File.Exists("whitelist.txt")) {
                File.AppendAllText(WhiteListLocation, "\r\n" + name);
            } else {
                File.WriteAllLines(WhiteListLocation, new string[] {
                    "# If whitelist mode is enabled in the config, then only",
                    "# names specified in this file will be allowed to join.",
                    "# Write one name per line. Anything after a # is a comment.",
                    "# Blank lines are ignored. Not case-sensitive.",
                    "# Leading/Trailing tabs and spaces are ignored.",
                    "",
                    name
                });
            }
        }

        public static void ShowPlayerConnectedMessage(Player player) {
            SendToAll(String.Format("{0}{1} ({2}{3}{0}) has joined the server.",
                                      Color.Sys,
                                      player.name,
                                      player.info.playerClass.color,
                                      player.info.playerClass.name),
                                      player);
            Logger.Log(String.Format("{0} ({1}|{2}) has joined the server.",
                                      player.name,
                                      player.info.playerClass.name,
                                      player.id), LogType.UserActivity);
        }

        // Return player count
        public static int GetPlayerCount() {
            return sessions.Count;
        }

        // Add a newly-logged-in player to the list, and notify existing players.
        public static bool RegisterPlayer(Player player) {
            lock (playerListLock) {
                if (players.Count >= Config.GetInt(ConfigKey.MaxPlayers) && !player.info.playerClass.reservedSlot) {
                    return false;
                }
                for (int i = 0; i < 255; i++) {
                    if (!players.ContainsKey(i)) {
                        player.id = i;
                        players[i] = player;
                        Server.UpdatePlayerList();
                        return true;
                    }
                }
                return false;
            }
        }


        // Remove player from the list, and notify remaining players
        public static void UnregisterPlayer(Player player) {
            lock (playerListLock) {
                if (players.ContainsKey(player.id)) {
                    SendToAll(PacketWriter.MakeDestroyEntity(player));
                    SendToAll(Color.Sys + player.GetLogName() + " left the server.");
                    for(int x = player.LoadedChunksMinX; x< (player.LoadedChunksMinX + Player.ChunkArea); x++) {
                        for (int z = player.LoadedChunksMinZ; z < (player.LoadedChunksMinZ + Player.ChunkArea); z++) {
                            world.GetChunk(x, z).RemovePlayer(player);
                        }
                    }
                    if(player.DisconnectReason == null)
                        Logger.Log("{0} left the server.", LogType.UserActivity, player.GetLogName());
                    else
                        Logger.Log("{0} left the server. ({1})", LogType.UserActivity, player.GetLogName(), player.DisconnectReason);

                    if (player.session.hasRegistered) {
                        players.Remove(player.id);
                        UpdatePlayerList();
                    }

                    PlayerDB.ProcessLogout(player);
                    PlayerDB.Save();
                } else {
                    Logger.Log("World.UnregisterPlayer: Trying to unregister a non-existent (unknown id) player.", LogType.Warning);
                }
            }
        }


        public static void UpdatePlayerList() {
            lock (playerListLock) {
                Player[] newPlayerList = new Player[players.Count];
                int i = 0;
                foreach (Player player in players.Values) {
                    newPlayerList[i++] = player;
                }
                playerList = newPlayerList;
            }
        }

        // Find player by name using autocompletion
        public static Player FindPlayer(string name) {
            if (name == null) return null;
            Player[] tempList = playerList;
            Player result = null;
            for (int i = 0; i < tempList.Length; i++) {
                if (tempList[i] != null && tempList[i].name.StartsWith(name, StringComparison.OrdinalIgnoreCase)) {
                    if (result == null) {
                        result = tempList[i];
                    } else {
                        return null;
                    }
                }
            }
            return result;
        }


        // Find player by name using autocompletion
        public static Player FindPlayer(System.Net.IPAddress ip) {
            Player[] tempList = playerList;
            for (int i = 0; i < tempList.Length; i++) {
                if (tempList[i] != null && tempList[i].session.GetIP().ToString() == ip.ToString()) {
                    return tempList[i];
                }
            }
            return null;
        }


        // Get player by name without autocompletion
        public static Player FindPlayerExact(string name) {
            Player[] tempList = playerList;
            for (int i = 0; i < tempList.Length; i++) {
                if (tempList[i] != null && tempList[i].name == name) {
                    return tempList[i];
                }
            }
            return null;
        }


        // Get player by name without autocompletion
        public static Player FindPlayerByNick(string nick) {
            Player[] tempList = playerList;
            for (int i = 0; i < tempList.Length; i++) {
                if (tempList[i] != null && tempList[i].nick == nick) {
                    return tempList[i];
                }
            }
            return null;
        }

        // Check if players are in same loaded chunk range
        public static bool CommonChunks(Player ply1, Player ply2) {
            if ((ply1.LoadedChunksMinX + Player.ChunkArea) <= ply2.LoadedChunksMinX)
                return false;
            if (ply2.LoadedChunksMinX >= (ply2.LoadedChunksMinX + Player.ChunkArea))
                return false;
            if ((ply1.LoadedChunksMinZ + Player.ChunkArea) <= ply2.LoadedChunksMinZ)
                return false;
            if (ply2.LoadedChunksMinZ >= (ply2.LoadedChunksMinZ + Player.ChunkArea))
                return false;
            return true;
        }

        #endregion

        #region Utilities

        static void GenerateServerID() {
            // generate random id
            Random rand = new Random();
            int idLength = rand.Next(12, 16);
            string idChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.~";
            ServerID = "";
            for (int i = 0; i < idLength; i++) {
                ServerID += idChars[rand.Next(0, idChars.Length - 1)];
            }
        }

        #endregion

    }
}
